package ru.btabank.dom.xpath.parsers;

import ru.btabank.dom.xpath.expression.AbsoluteLocationPath;
import ru.btabank.dom.xpath.expression.AttributeStep;
import ru.btabank.dom.xpath.expression.BooleanFunction;
import ru.btabank.dom.xpath.expression.ChildStep;
import ru.btabank.dom.xpath.expression.CommentStep;
import ru.btabank.dom.xpath.expression.CountFunction;
import ru.btabank.dom.xpath.expression.Expression;
import ru.btabank.dom.xpath.expression.FunctionCall;
import ru.btabank.dom.xpath.expression.Literal;
import ru.btabank.dom.xpath.expression.LocalNameFunction;
import ru.btabank.dom.xpath.expression.LocationPath;
import ru.btabank.dom.xpath.expression.NameFunction;
import ru.btabank.dom.xpath.expression.NamespaceUriFunction;
import ru.btabank.dom.xpath.expression.NumberFunction;
import ru.btabank.dom.xpath.expression.NumberLiteral;
import ru.btabank.dom.xpath.expression.ParentStep;
import ru.btabank.dom.xpath.expression.RelativeLocationPath;
import ru.btabank.dom.xpath.expression.SelfStep;
import ru.btabank.dom.xpath.expression.Step;
import ru.btabank.dom.xpath.expression.StringFunction;
import ru.btabank.dom.xpath.expression.StringLiteral;
import ru.btabank.dom.xpath.expression.TextStep;
import ru.btabank.dom.xpath.namespace.NamespaceMap;
import ru.btabank.dom.xpath.namespace.UnresolvableException;

public class ExpressionParser extends Parser {
    private Expression expression;

    private NamespaceMap namespaceMap;

    public ExpressionParser() {
        super();
        this.expression = null;
        this.namespaceMap = null;
    }

    public void setNamespaceMap(NamespaceMap namespaceMap) {
        this.namespaceMap = namespaceMap;
    }

    public void parse(String string) throws ParsingException {
        super.parse(string);

        try {
            this.expression = expression();
        } catch (UnresolvableException exception) {
            throw new ParsingException(exception.getMessage());
        }

        if (lookAhead(1).getType() != Token.END_OF_EXPRESSION_TOKEN)
            throw new UnexpectedTokenException(lookAhead(1));
    }

    public Expression getExpression() {
        return this.expression;
    }

    // [1] Expression ::= Literal
    // | FunctionCall
    // | LocationPath
    // [2] Literal ::= StringLiteral
    // | NumberLiteral

    private Expression expression() throws SyntaxException, UnresolvableException {
        switch (lookAhead(1).getType()) {
        case Token.STRING_LITERAL_TOKEN:
            return stringLiteral();

        case Token.NUMBER_LITERAL_TOKEN:
            return numberLiteral();

        case Token.SLASH_TOKEN:
            return absoluteLocationPath();

        case Token.STAR_TOKEN:
        case Token.DOT_TOKEN:
        case Token.DOUBLE_DOT_TOKEN:
        case Token.AT_TOKEN:
            return relativeLocationPath();

        case Token.IDENTIFIER_TOKEN:
            if (lookAhead(2).getType() == Token.LEFT_PARENTHESIS_TOKEN
                && lookAhead(3).getType() != Token.RIGHT_PARENTHESIS_TOKEN)
                return functionCall();
            else
                return relativeLocationPath();

        case Token.END_OF_EXPRESSION_TOKEN:
            throw new EndOfExpressionException();

        default:
            throw new UnexpectedTokenException(lookAhead(1));
        }
    }

    // [3] StringLiteral ::= '"'[^"]*'"'

    private Literal stringLiteral() throws SyntaxException {
        String value = match(Token.STRING_LITERAL_TOKEN).getValue();
        return new StringLiteral(value);
    }

    // [4] NumberLiteral ::= [0-9]+
    // | [0-9]+ '.' [0-9]+

    private Literal numberLiteral() throws SyntaxException {
        String value = match(Token.NUMBER_LITERAL_TOKEN).getValue();
        if (lookAhead(1).getType() == Token.DOT_TOKEN) {
            value = value + match(Token.DOT_TOKEN).getValue() + match(Token.NUMBER_LITERAL_TOKEN).getValue();
        }
        return new NumberLiteral(Double.valueOf(value));
    }

    // [5] FunctionCall ::= FunctionName '(' Expression ')'
    // [6] FunctionName ::= 'name'
    // | 'name-local'
    // | 'namespace-uri'
    // | 'count'
    // | 'boolean'
    // | 'string'
    // | 'number'

    private FunctionCall functionCall() throws SyntaxException, UnresolvableException {
        FunctionCall functionCall;
        String functionName = match(Token.IDENTIFIER_TOKEN).getValue();

        if (functionName.equals("boolean"))
            functionCall = new BooleanFunction();
        else if (functionName.equals("string"))
            functionCall = new StringFunction();
        else if (functionName.equals("number"))
            functionCall = new NumberFunction();
        else if (functionName.equals("name"))
            functionCall = new NameFunction();
        else if (functionName.equals("local-name"))
            functionCall = new LocalNameFunction();
        else if (functionName.equals("count"))
            functionCall = new CountFunction();
        else if (functionName.equals("namespace-uri"))
            functionCall = new NamespaceUriFunction();
        else
            throw new SyntaxException(
                                      "Unknown function '"
                                          + functionName
                                          + "'. Expected 'boolean', 'string', 'number', 'name', 'local-name', 'count', or 'namespace-uri'");

        match(Token.LEFT_PARENTHESIS_TOKEN);
        functionCall.addParameter(expression());
        match(Token.RIGHT_PARENTHESIS_TOKEN);

        return functionCall;
    }

    // [8] AbsoluteLocationPath ::= '/' RelativeLocationPath

    private LocationPath absoluteLocationPath() throws SyntaxException, UnresolvableException {
        AbsoluteLocationPath locationPath = new AbsoluteLocationPath();
        while (lookAhead(1).getType() == Token.SLASH_TOKEN) {
            match(Token.SLASH_TOKEN);
            locationPath.addStep(step());
        }
        return locationPath;
    }

    // [9] RelativeLocationPath ::= Step | Step '/' RelativeLocationPath

    private LocationPath relativeLocationPath() throws SyntaxException, UnresolvableException {
        RelativeLocationPath locationPath = new RelativeLocationPath();
        locationPath.addStep(step());
        while (lookAhead(1).getType() == Token.SLASH_TOKEN) {
            match(Token.SLASH_TOKEN);
            locationPath.addStep(step());
        }
        return locationPath;
    }

    // [10] Step ::= ElementStep
    // | AttributeStep
    // | ParentStep
    // | SelfStep
    // | TextStep
    // | CommentStep

    private Step step() throws SyntaxException, UnresolvableException {
        switch (lookAhead(1).getType()) {

        case Token.IDENTIFIER_TOKEN:
            if (lookAhead(2).getType() == Token.LEFT_PARENTHESIS_TOKEN)
                return kindStep();
            else
                return elementStep();

        case Token.STAR_TOKEN:
            return elementStep();

        case Token.AT_TOKEN:
            return attributeStep();

        case Token.DOUBLE_DOT_TOKEN:
            return parentStep();

        case Token.DOT_TOKEN:
            return selfStep();

        case Token.END_OF_EXPRESSION_TOKEN:
            throw new EndOfExpressionException();

        default:
            throw new UnexpectedTokenException(lookAhead(1));
        }
    }

    // [11] ElementStep ::= NameTest Predicate?
    // [12] Predicate ::= '[' NumberLiteral ']'
    // [18] NameTest ::= '*'
    // | '*:' NCName
    // | NCName ':*'
    // | NCName ':' NCName
    // | NCName

    private Step elementStep() throws SyntaxException, UnresolvableException {
        String prefix = null;
        String localName = null;
        String namespaceURI = null;
        Integer predicate = null;

        switch (lookAhead(1).getType()) {
        case Token.STAR_TOKEN:
            if (lookAhead(2).getType() == Token.COLON_TOKEN) {
                prefix = match(Token.STAR_TOKEN).getValue();
                match(Token.COLON_TOKEN);
                localName = match(Token.IDENTIFIER_TOKEN).getValue();
            } else
                localName = match(Token.STAR_TOKEN).getValue();
            break;

        case Token.IDENTIFIER_TOKEN:
            if (lookAhead(2).getType() == Token.COLON_TOKEN) {
                prefix = match(Token.IDENTIFIER_TOKEN).getValue();
                namespaceURI = namespaceMap.resolve(prefix);
                match(Token.COLON_TOKEN);
                if (lookAhead(1).getType() == Token.IDENTIFIER_TOKEN)
                    localName = match(Token.IDENTIFIER_TOKEN).getValue();
                else
                    localName = match(Token.STAR_TOKEN).getValue();
            } else
                localName = match(Token.IDENTIFIER_TOKEN).getValue();
            break;

        default:
            throw new UnexpectedTokenException(lookAhead(1));
        }

        if (lookAhead(1).getType() == Token.LEFT_BRACKET_TOKEN) {
            match(Token.LEFT_BRACKET_TOKEN);
            predicate = new Integer(match(Token.NUMBER_LITERAL_TOKEN).getValue());
            match(Token.RIGHT_BRACKET_TOKEN);
        }

        return new ChildStep(prefix, localName, namespaceURI, predicate);
    }

    // [13] AttributeStep ::= '@' NameTest
    // [18] NameTest ::= '*'
    // | '*:' NCName
    // | NCName ':*'
    // | NCName ':' NCName
    // | NCName

    private Step attributeStep() throws SyntaxException, UnresolvableException {
        String prefix = null;
        String localName = null;
        String namespaceURI = null;

        match(Token.AT_TOKEN);

        switch (lookAhead(1).getType()) {
        case Token.STAR_TOKEN:
            if (lookAhead(2).getType() == Token.COLON_TOKEN) {
                prefix = match(Token.STAR_TOKEN).getValue();
                match(Token.COLON_TOKEN);
                localName = match(Token.IDENTIFIER_TOKEN).getValue();
            } else
                localName = match(Token.STAR_TOKEN).getValue();
            break;

        case Token.IDENTIFIER_TOKEN:
            if (lookAhead(2).getType() == Token.COLON_TOKEN) {
                prefix = match(Token.IDENTIFIER_TOKEN).getValue();
                namespaceURI = namespaceMap.resolve(prefix);
                match(Token.COLON_TOKEN);
                if (lookAhead(1).getType() == Token.IDENTIFIER_TOKEN)
                    localName = match(Token.IDENTIFIER_TOKEN).getValue();
                else
                    localName = match(Token.STAR_TOKEN).getValue();
            } else
                localName = match(Token.IDENTIFIER_TOKEN).getValue();
            break;

        default:
            throw new UnexpectedTokenException(lookAhead(1));
        }

        return new AttributeStep(prefix, localName, namespaceURI);
    }

    // [14] ParentStep ::= '..'

    private Step parentStep() throws SyntaxException {
        match(Token.DOUBLE_DOT_TOKEN);
        return new ParentStep();
    }

    // [15] SelfStep ::= '.'

    private Step selfStep() throws SyntaxException {
        match(Token.DOT_TOKEN);
        return new SelfStep();
    }

    // [16] TextStep ::= 'text' '(' ')'
    // [17] CommentStep ::= 'comment' '(' ')'

    private Step kindStep() throws SyntaxException {
        Step step;
        String nodeTest;

        nodeTest = match(Token.IDENTIFIER_TOKEN).getValue();
        if (nodeTest.equals("text"))
            step = new TextStep();
        else if (nodeTest.equals("comment"))
            step = new CommentStep();
        else
            throw new SyntaxException("Unknown kind of node '" + nodeTest
                                      + "()'. Expected 'text()' or 'comment()'");

        match(Token.LEFT_PARENTHESIS_TOKEN);
        match(Token.RIGHT_PARENTHESIS_TOKEN);
        return step;
    }
}
